/*---------------------------------------------------------------------------
 * Copyright (C) 2002 Dallas Semiconductor Corporation, All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY,  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL DALLAS SEMICONDUCTOR BE LIABLE FOR ANY CLAIM, DAMAGES
 * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * Except as contained in this notice, the name of Dallas Semiconductor
 * shall not be used except as stated in the Dallas Semiconductor
 * Branding Policy.
 *---------------------------------------------------------------------------
 */
package com.dalsemi.onewire.application.monitor;

import java.util.Hashtable;
import java.util.Vector;
import java.util.Enumeration;

import com.dalsemi.onewire.OneWireException;
import com.dalsemi.onewire.adapter.OneWireIOException;
import com.dalsemi.onewire.adapter.DSPortAdapter;
import com.dalsemi.onewire.container.OneWireContainer;
import com.dalsemi.onewire.utils.Address;
import com.dalsemi.onewire.utils.OWPath;

/**
 * <P>Abstract super-class for 1-Wire Monitors, a optionally-threadable
 * object for searching 1-Wire networks.  If this object is not run in it's own
 * thread, it is possible to perform single-step searches by calling the search
 * method directly {@see #search(Vector, Vector)}.  The monitor will generate
 * events for device arrivals, device departures, and exceptions from the
 * DSPortAdapter.</P>
 *
 * <P>In a touch-contact environment, it is not suitable to say that a
 * device has "departed" because it was missing for one cycle of searching.
 * In the time it takes to get an iButton into a blue-dot receptor, the
 * monitor could have generated a handful of arrival and departure events.  To
 * circumvent this problem, the device monitor keeps a "missing state count" for
 * each device on the network.  Each search cycle that passes where the device
 * is missing causes it's "missing state count" to be incremented.  Once the
 * device's "missing state count" is equal to the "max state count"
 * {@see #getMaxStateCount()}, a departure event is generated for the device.
 * If the 1-Wire Network is not in a touch environment, it may be unnecessary
 * to use this "missing state count".  In those instances, setting the state
 * count to 1 will disable the feature {@see #setMaxStateCount(int)}.</P>
 *
 * <P>Similarly, the reporting of exceptions could be spurious in a
 * touch-contact environment.  Instead of reporting the exception on each
 * failed search attempt, the monitor will default to retrying the search a
 * handful of times {@see #getMaxErrorCount()} before finally reporting the
 * exception.  To disable this feature, set the max error count to 1
 * {@see #setMaxErrorCount(int)}.</P>
 *
 * <P>To receive events, an object must implement the
 * <code>DeviceMonitorEventListener</code> interface
 * {@see DeviceMonitorEventListener} and must be added to
 * the list of listeners {@see #addDeviceMonitorEventListener}.</P>
 *
 * @author SH
 * @version 1.00
 */
public abstract class AbstractDeviceMonitor
   implements Runnable
{
   //--------
   //-------- Constants
   //--------

   /** object used for synchronization */
   protected final Object sync_flag = new Object();

   /** Addresses of all current devices, mapped to their state count */
   protected final Hashtable deviceAddressHash = new Hashtable();

   /**
    * hashtable for holding device containers, static to keep only a
    * single instance of each OneWireContainer.
    */
   protected static final Hashtable deviceContainerHash = new Hashtable();

   /**
    * Listeners who receive notification of events generated by this
    * device Monitor
    */
   protected final Vector listeners = new Vector();

   //--------
   //-------- Variables
   //--------

   /** Number of searches that a button should be "missing" before it is removed */
   protected int max_state_count = 3;

   /** Number of searches that an error occurs before a dialog is displayed */
   protected int max_error_count = 6;

   /** Flag for overall thread running state */
   protected volatile boolean keepRunning = true, hasCompletelyStopped = false;

   /** Flag to indicate thread has begin to run */
   protected volatile boolean startRunning = true;

   /** Flag to indicate thread is running now */
   protected volatile boolean isRunning = false;

   /** the adapter to search for devices */
   protected DSPortAdapter adapter = null;

   /**
    * The device monitor will internally cache OneWireContainer objects for each
    * 1-Wire device.  Use this method to clean up all stale container objects.
    * A stale container object is a OneWireContainer object which references a
    * 1-Wire device address which has not been seen by a recent search.
    * This will be essential in a touch-contact environment which could run
    * for some time and needs to conserve memory.
    */
   public void cleanUpStaleContainerReferences()
   {
      synchronized(deviceContainerHash)
      {
         Enumeration e = deviceContainerHash.keys();
         while(e.hasMoreElements())
         {
            Object o = e.nextElement();
            if(!deviceAddressHash.containsKey(o))
               deviceContainerHash.remove(o);
         }
      }
   }

   /**
    * The device monitor will internally cache OWPath objects for each
    * 1-Wire device.  Use this method to clean up all stale OWPath objects.
    * A stale path object is a OWPath which references a branching path to a
    * 1-Wire device address which has not been seen by a recent search.
    * This will be essential in a touch-contact environment which could run
    * for some time and needs to conserve memory.
    */
   public void cleanUpStalePathReferences()
   {
      // no-op by default.  Only NetworkDeviceMonitor uses paths
   }

   /**
    * Resets this device monitor.  All known devices will be marked as
    * "departed" and departure events will be fired.
    */
   public void resetSearch()
   {
      synchronized (sync_flag)
      {
         // fire departures for all devices
         if(deviceAddressHash.size()>0 && listeners.size()>0)
         {
            Vector v = new Vector(deviceAddressHash.size());
            Enumeration e = deviceAddressHash.keys();
            while(e.hasMoreElements())
               v.addElement(e.nextElement());
            fireDepartureEvent(adapter, v);
         }

         deviceAddressHash.clear();
      }
   }

   /**
    * The number of searches that a button should be "missing"
    * before it is removed.
    *
    * @return The number of searches that a button should be "missing"
    * before it is removed.
    */
   public int getMaxStateCount()
   {
      return this.max_state_count;
   }

   /**
    * The number of searches that a button should be "missing"
    * before it is removed
    *
    * @param stateCnt The number of searches that a button should be "missing"
    * before it is removed.
    */
   public void setMaxStateCount(int stateCnt)
   {
      if(stateCnt<=0)
         throw new IllegalArgumentException("State Count must be greater than 0");

      this.max_state_count = stateCnt;
   }

   /**
    * Number of searches that an error occurs before listener's are notified
    *
    * @return Number of searches that an error occurs before listener's
    * are notified
    */
   public int getMaxErrorCount()
   {
      return this.max_error_count;
   }

   /**
    * Number of searches that an error occurs before listener's are notified
    *
    * @param errorCnt Number of searches that an error occurs before listener's
    * are notified
    */
   public void setMaxErrorCount(int errorCnt)
   {
      if(errorCnt<=0)
         throw new IllegalArgumentException("Error Count must be greater than 0");

      this.max_error_count = errorCnt;
   }


   /**
    * Returns the DSPortAdapter this device is searching
    *
    * @param the DSPortAdapter this monitor is searching
    */
   public DSPortAdapter getAdapter()
   {
      return this.adapter;
   }

   /**
    * Sets this monitor to search a new DSPortAdapter
    *
    * @param the DSPortAdapter this monitor should search
    */
   public abstract void setAdapter(DSPortAdapter adapter);

   //--------
   //-------- Monitor methods
   //--------

   /**
    * Performs a search of the 1-Wire network
    *
    * @param arrivals A vector of Long objects, represent new arrival addresses.
    * @param departures A vector of Long objects, represent departed addresses.
    */
   public abstract void search(Vector arrivals, Vector departures)
      throws OneWireException, OneWireIOException;

   /**
    * Pause this monitor
    *
    * @param blocking if true, this method will block until the monitor is paused.
    * @returns true if the monitor was successfully paused.
    */
   public boolean pauseMonitor (boolean blocking)
   {
      // clear the start flag
      synchronized (sync_flag)
      {
         if (hasCompletelyStopped || (!startRunning && !isRunning))
            return true;

         startRunning = false;
      }


      // wait until it is paused or until timeout
      int i = 0;
      while (isRunning && (blocking || (i++)<100))
      {
         msSleep(10);
      }

      return !isRunning;
   }

   /**
    * Resume this monitor
    *
    * @param blocking if true, this method will block until the monitor is resumed.
    * @returns true if the monitor was successfully resumed.
    */
   public boolean resumeMonitor (boolean blocking)
   {
      // set the start flag
      synchronized (sync_flag)
      {
         if (hasCompletelyStopped)
            return false;
         if (startRunning && isRunning)
            return true;

         startRunning = true;
      }

      // wait until it is running
      int i = 0;
      while (!isRunning && (blocking || (i++)<100))
      {
         msSleep(10);
      }

      return isRunning;
   }

   /**
    * Check if this monitor is running.
    *
    * @return <CODE>true</CODE> if monitor is running
    */
   public boolean isMonitorRunning ()
   {
      return isRunning;
   }

   /**
    * Kill this monitor.  Wait util this
    * thread is no longer alive (with timeout).
    */
   public void killMonitor ()
   {
      // clear the running flags
      synchronized (sync_flag)
      {
         keepRunning  = false;
         startRunning = false;
      }

      // wait until the thread is no longer running, with a timeout
      int i=0;
      while (!hasCompletelyStopped && i<100)
      {
         msSleep(20);
      }
   }

   /**
    * Monitor run method that performs a periodic search
    * of the entire 1-Wire network.  Listeners
    * that have registered are notified when changes in the network
    * are detected.
    */
   public void run ()
   {
      synchronized (sync_flag)
      {
         hasCompletelyStopped = false;
      }

      Vector arrivals = new Vector(), departures = new Vector();
      while (keepRunning)
      {
         if (startRunning)
         {
            // set is now running
            synchronized (sync_flag)
            {
               isRunning = true;
            }

            // erase previous arrivals and departures
            arrivals.setSize(0);
            departures.setSize(0);

            // number of times an error occurred during 1-Wire search
            int errorCount = 0;
            boolean done = false;
            while(!done)
            {
               try
               {
                  // search for new devices, remove stale device entries
                  search(arrivals, departures);
                  done = true;
               }
               catch(Exception e)
               {
                  if(++errorCount==max_error_count)
                  {
                     fireException(adapter, e);
                     done = true;
                  }
               }
            }

            // sleep to give other threads a chance at this network
            msSleep(200);
         }
         else
         {
            // not running so clear flag
            synchronized (sync_flag)
            {
               isRunning = false;
            }
            msSleep(200);
         }
      }
      // not running so clear flag
      synchronized (sync_flag)
      {
         isRunning = false;
         hasCompletelyStopped = true;
      }
   }

   //--------
   //-------- Utility methods
   //--------

   /**
    * Sleep for the specified number of milliseconds
    *
    * @param msTime number of milliseconds to sleep
    */
   protected void msSleep (long msTime)
   {
      Thread.yield();
      try
      {
         Thread.sleep(msTime);
      }
      catch (InterruptedException e)
      { ; }
   }

   //--------
   //-------- Event methods
   //--------

   /**
    * Add a listener, to be notified of arrivals, departures, and exceptions.
    *
    * @param dmel Listener of monitor events.
    */
   public void addDeviceMonitorEventListener(DeviceMonitorEventListener dmel)
   {
      if(dmel!=null)
         this.listeners.addElement(dmel);
   }

   /**
    * Notify the listeners of the arrival event
    *
    * @param address Vector of Long objects representing the address of new
    * arrivals.
    */
   protected void fireArrivalEvent(DSPortAdapter adapter, Vector address)
   {
      DeviceMonitorEvent dme =
         new DeviceMonitorEvent(DeviceMonitorEvent.ARRIVAL, this,
                                adapter, (Vector)address.clone());
      for (int i = 0; i < listeners.size(); i++)
      {
         DeviceMonitorEventListener listener
            = (DeviceMonitorEventListener)listeners.elementAt(i);
         listener.deviceArrival(dme);
      }
   }

   /**
    * Notify the listeners of the departure event
    *
    * @param address Vector of Long objects representing the address of
    * departed devices.
    */
   protected void fireDepartureEvent(DSPortAdapter adapter, Vector address)
   {
      DeviceMonitorEvent dme =
         new DeviceMonitorEvent(DeviceMonitorEvent.DEPARTURE, this,
                                adapter, (Vector)address.clone());

      for (int i = 0; i < listeners.size(); i++)
      {
         DeviceMonitorEventListener listener
            = (DeviceMonitorEventListener)listeners.elementAt(i);
         listener.deviceDeparture(dme);
      }
   }

   /**
    * Notify the listeners of the exception
    *
    * @param ex The exception that occurred.
    */
   private void fireException (DSPortAdapter adapter, Exception ex)
   {
      for (int i = 0; i < listeners.size(); i++)
      {
         ((DeviceMonitorEventListener) listeners.elementAt(
            i)).networkException(
               new DeviceMonitorException(this, adapter, ex));
      }
   }

   /**
    * Returns the OWPath of the device with the given address.
    *
    * @param address a byte array representing the address of the device
    * @return The OWPath representing the network path to the device.
    */
   public OWPath getDevicePath(byte[] address)
   {
      return getDevicePath(Address.toLong(address));
   }

   /**
    * Returns the OWPath of the device with the given address.
    *
    * @param address a string representing the address of the device
    * @return The OWPath representing the network path to the device.
    */
   public OWPath getDevicePath(String address)
   {
      return getDevicePath(Address.toLong(address));
   }

   /**
    * Returns the OWPath of the device with the given address.
    *
    * @param address a long representing the address of the device
    * @return The OWPath representing the network path to the device.
    */
   public OWPath getDevicePath(long address)
   {
      return getDevicePath(new Long(address));
   }

   /**
    * Returns the OWPath of the device with the given address.
    *
    * @param address a Long object representing the address of the device
    * @return The OWPath representing the network path to the device.
    */
   public abstract OWPath getDevicePath(Long address);

   /**
    * Returns all addresses known by this monitor as an Enumeration of Long
    * objects.
    *
    * @return Enumeration of Long objects
    */
   public Enumeration getAllAddresses()
   {
      return deviceAddressHash.keys();
   }

   //--------
   //-------- Static methods
   //--------

   /**
    * Returns the OneWireContainer object of the device with the given address.
    *
    * @param adapter The DSPortAdapter that the device is connected to.
    * @param address a byte array representing the address of the device
    * @return The specific OneWireContainer object of the device
    */
   public static OneWireContainer getDeviceContainer(DSPortAdapter adapter,
                                                     byte[] address)
   {
      return getDeviceContainer(adapter, Address.toLong(address));
   }

   /**
    * Returns the OneWireContainer object of the device with the given address.
    *
    * @param adapter The DSPortAdapter that the device is connected to.
    * @param address a String representing the address of the device
    * @return The specific OneWireContainer object of the device
    */
   public static OneWireContainer getDeviceContainer(DSPortAdapter adapter,
                                                     String address)
   {
      return getDeviceContainer(adapter, Address.toLong(address));
   }

   /**
    * Returns the OneWireContainer object of the device with the given address.
    *
    * @param adapter The DSPortAdapter that the device is connected to.
    * @param address a long representing the address of the device
    * @return The specific OneWireContainer object of the device
    */
   public static OneWireContainer getDeviceContainer(DSPortAdapter adapter,
                                                     long address)
   {
      return getDeviceContainer(adapter, new Long(address));
   }

   /**
    * Returns the OneWireContainer object of the device with the given address.
    *
    * @param adapter The DSPortAdapter that the device is connected to.
    * @param address a Long object representing the address of the device
    * @return The specific OneWireContainer object of the device
    */
   public static OneWireContainer getDeviceContainer(DSPortAdapter adapter,
                                                     Long longAddress)
   {
      synchronized(deviceContainerHash)
      {
         Object o = deviceContainerHash.get(longAddress);
         if(o==null)
         {
            o = adapter.getDeviceContainer(longAddress.longValue());
            putDeviceContainer(longAddress, (OneWireContainer)o);
         }
         return (OneWireContainer)o;
      }
   }

   /**
    * Sets the OneWireContainer object of the device with the given address.
    *
    * @param address a byte array object representing the address of the device
    * @param owc The specific OneWireContainer object of the device
    */
   public static void putDeviceContainer(byte[] address, OneWireContainer owc)
   {
      putDeviceContainer(Address.toLong(address),owc);
   }

   /**
    * Sets the OneWireContainer object of the device with the given address.
    *
    * @param address a String object representing the address of the device
    * @param owc The specific OneWireContainer object of the device
    */
   public static void putDeviceContainer(String address, OneWireContainer owc)
   {
      putDeviceContainer(Address.toLong(address),owc);
   }

   /**
    * Sets the OneWireContainer object of the device with the given address.
    *
    * @param address a long object representing the address of the device
    * @param owc The specific OneWireContainer object of the device
    */
   public static void putDeviceContainer(long address, OneWireContainer owc)
   {
      putDeviceContainer(new Long(address),owc);
   }

   /**
    * Sets the OneWireContainer object of the device with the given address.
    *
    * @param address a Long object representing the address of the device
    * @param owc The specific OneWireContainer object of the device
    */
   public static void putDeviceContainer(Long longAddress, OneWireContainer owc)
   {
      synchronized(deviceContainerHash)
      {
         deviceContainerHash.put(longAddress, owc);
      }
   }
}